#!/bin/bash
# -*- coding: utf-8 -*-
# --------------------------------------------------------
# Name:            start.py  
# Purpose:  基于已知的训练集，"测试集"中的user  对  item进行评分预测.
#
# Language:    Python 3.2
# Author:      liangzhicheng
# Author:      liuli
# Created:     14-06-2016
# --------------------------------------------------------
import heapq
from math import sqrt


### 计算pearson相关度  两个用户之间的相似度
def sim_pearson(prefer, person1, person2):
    sim = {}
    # 查找双方都评价过的项
    for item in prefer[person1]:
        if item in prefer[person2]:
            sim[item] = 1  # 将相同项添加到字典sim中  存放两个用户共同评价过的项
    # 元素个数
    n = len(sim)  # 双方都评价过的项目个数
    if len(sim) == 0:  # 如果双方共同评价过的项目个数为0，则返回-1，说明双方没有共同的喜好
        return -1

    # 所有偏好之和
    sum1 = sum([prefer[person1][item] for item in sim])  # 计算各自在共同评价过的物品中的喜好评分各自求和
    sum2 = sum([prefer[person2][item] for item in sim])

    # 求平方和
    sum1Sq = sum([pow(prefer[person1][item], 2) for item in sim])  # 计算各自在共同评价过的物品中的喜好评分各自求平方和
    sum2Sq = sum([pow(prefer[person2][item], 2) for item in sim])

    # 求乘积之和 ∑XiYi
    sumMulti = sum([prefer[person1][item] * prefer[person2][item] for item in sim])  # 求双方乘积的和

    num1 = sumMulti - (sum1 * sum2 / n)  # 用乘积和 减去 两个偏好和乘积除以n
    num2 = sqrt((sum1Sq - pow(sum1, 2) / n) * (sum2Sq - pow(sum2, 2) / n))
    if num2 == 0:  # 如果分母为0，本处将返回0.
        return 0

    result = num1 / num2  # 最终得到的相关系数
    return result


### 获取对item评分过的并且与当前person用户 最相似的k个用户（K默认20）
def topKMatches(prefer, person, itemId, k=50, sim=sim_pearson):
    userSet = []
    # 找出所有prefer中评价过Item的用户,存入userSet
    for user in prefer:
        if itemId in prefer[user]:
            userSet.append(user)
    # 计算相似性 相似用户计算过程中 不加入当前用户 相似度计算选用皮尔森相关系数法
    scores = [(sim(prefer, person, other), other) for other in userSet if other != person]
    rs = heapq.nlargest(k, scores)  # 用最大堆求最大的k个元素
    # users = [i[1] for i in rs]  # 返回最相似的前k个users的userId
    # return users # 只返回userId不好 后面 还会需要用的相似度
    return rs


#####下面的代码可以暂时不用，最大堆求前k个性能更好 ######
# # 按相似度排序从小到大
# scores.sort(reverse=True)
# if len(scores) <= k:  # 如果小于k，只选择这些做推荐。
#     for item in scores:
#         users.append(item[1])  # 提取每项的userId
#     return users
# else:  # 如果>k,截取k个用户
#     kscore = scores[0:k]
#     for item in kscore:
#         users.append(item[1])  # 提取每项的userId
#     return users  # 返回K个最相似用户的ID


### 计算用户的平均评分
def getAverage(prefer, userId):
    count = 0
    sum = 0
    # print("传入UserId", userId)
    if userId not in prefer.keys(): # 如果训练集中不存在该用户 直接返回 均分2.5
        return 2.5
    for item in prefer[userId]:
        # print("item", item)
        sum = sum + prefer[userId][item]
        # print("prefer[userId][item]", prefer[userId][item])
        count = count + 1
    return sum / count


### 平均加权策略，预测userId对itemId的评分
def getRating(prefer1, userId, itemId, knumber=50, similarity=sim_pearson):
    # 获取K近邻用户(评过分的用户集) 最相似的k个用户
    users = topKMatches(prefer1, userId, itemId, k=knumber, sim=sim_pearson)
    jiaquanAverage = 0.0
    simSums = 0.0
    # 计算每个用户的加权，预测
    for other in users:
        sim = other[0]  # 计算比较其他用户的相似度
        otherId = other[1]
        averageOther = getAverage(prefer1, otherId)  # 该用户的平均分
        # 累加
        simSums += abs(sim)  # 取绝对值
        jiaquanAverage += (prefer1[otherId][itemId] - averageOther) * sim  # 累加，一些值为负

    # 获取userId 的平均值 该用户给所有项目打分的均值
    averageOfUser = getAverage(prefer1, userId)
    # simSums为0，即该项目尚未被其他用户评分，这里的处理方法：返回用该户平均分
    if simSums == 0:
        return averageOfUser  # todo 这里需要改进 ！！！！
    else:
        return averageOfUser + jiaquanAverage / simSums


##==================================
#         加载指定的训练集文件
#  参数fileName 代表某个训练集文件
##==================================
def loadMovieLensTrain(fileName):
    prefer = {}
    for line in open(fileName, 'r'):  # 打开指定文件
        (userid, movieid, rating, ts) = line.split('\t')  # 数据集中每行有4项
        prefer.setdefault(userid, {})  # 设置字典的默认格式,元素是user:{}字典
        prefer[userid][movieid] = float(rating)
    return prefer


##==================================================================
##     getAllUserRating(): 获取所有用户的预测评分，存放到fileResult中
##
## 参数:fileTrain,fileTest 是训练文件和对应的测试文件，fileResult为结果文件
##     similarity是相似度度量方法，默认是皮尔森。
##==================================================================
def getAllUserRating(fileTrain, fileTest, fileResult, k):
    preferTrain = loadMovieLensTrain(fileTrain)  # 加载训练集 训练集的偏好矩阵
    preferTest = loadMovieLensTrain(fileTest)  # 加载测试集

    ##===================================================
    ##设定阀值选取评分高于3的用户可能感兴趣的项目向其推荐
    ##===================================================
    # 文件操作
    inAllnum = 0
    file = open(fileResult, 'a')
    # file.write("%s\n" % "用户id----项目ID------预测评分------实际评分-------")
    for userid in preferTest:  # test集中每个用户
        for item in preferTest[userid]:  # 对于test集合中每一个项目用base数据集,CF预测评分
            rating = getRating(preferTrain, userid, item, k)  # 基于训练集预测用户评分(用户数目<=K)
            # if rating > 3: # 改成大于用户的平均评分 就 推荐给他
            if rating > getAverage(preferTrain, userid):  # 改成大于用户在训练集中的的平均评分 就推荐给他
                file.write('%s\t%s\t%s\t%s\n' % (userid, item, rating, preferTest[userid][item]))
                inAllnum = inAllnum + 1
    file.close()
    print("-------------Completed!!---append to %s  %d 条数据" % (file.name, inAllnum))


############    主程序   ##############
if __name__ == "__main__":
    print(u"\n--------------start... -----------\n")
    for i in range(1, 6):
        baseName = 'u' + str(i) + '.base'
        testName = 'u' + str(i) + '.test'
        getAllUserRating('ml-100k/' + baseName, 'ml-100k/' + testName,
                         'recommend/recommend-more-than-history-rating.txt', 20)
    print(u"\n--------------end... -----------\n")
